#!/usr/bin/env python
# -*- coding: utf-8 -*-
# 时间     : 2019-07-31 20:16:01
# 作者     : DL (584054432@qq.com)
# 网址     : https://gitee.com/dingliangPython/
# 软件版本 : Python3.7.3
# 功能     ：西门子S7协议模块

import csv
import os
import struct
from struct import pack
import logging
from snap7.client import Client
from snap7.common import ADict




class S7Client(Client):
    """docstring for S7Client"""

    def __init__(self):
        super(S7Client, self).__init__()
        self.log = logging.getLogger("main.s7")

    # 写入plc数据
    def write_data(self, config, value):
        result = None
        area, db_number, begin, size, _type, _ = self.get_read_param(config)
        _type = _type.lower()


        if _type == 'byte':
            bin_vlue = pack('B', value)
            self.write_area(area, db_number, begin, bin_vlue)
            result = True

        # 字符串写入
        if _type == "string":
            bin_vlue = pack('{}s'.format(len(value)), str(value).encode())
            self.write_area(area, db_number, begin, bin_vlue)
            result = True

        return result

    # 解析数据
    def parse_data(self, data, param):
        _, _, _, _, data_type, index = param
        lenght = len(data)
        # 字符串
        if data_type == 'string':
            return self.parse_string(data)
        # 布尔
        if data_type in ('bool', 'bit'):
            return self.parse_bit(data, index)
        # byte,accii
        if data_type in ('byte', 'usint'):
            return data[0] if lenght == 1 else [v for v in data]
        if data_type == 'sint':
            data = struct.unpack('!{}b'.format(lenght), data)
            return data[0] if lenght == 1 else [v for v in data]
        # 短整形2字节
        if data_type in ('word', 'ushort'):
            data = struct.unpack('!{}H'.format(lenght // 2), data)
            return data[0] if lenght // 2 == 1 else [v for v in data]
        if data_type == 'short':
            data = struct.unpack('!{}h'.format(lenght // 2), data)
            return data[0] if lenght // 2 == 1 else [v for v in data]
        # 整形4字节
        if data_type in ('dword', 'uint'):
            data = struct.unpack('!{}I'.format(lenght // 4), data)
            return data[0] if lenght // 4 == 1 else [v for v in data]
        if data_type == 'int':
            data = struct.unpack('!{}i'.format(lenght // 4), data)
            return data[0] if lenght // 4 == 1 else [v for v in data]
        # 长整形4字节
        if data_type == 'ulong':
            data = struct.unpack('!{}L'.format(lenght // 4), data)
            return data[0] if lenght // 4 == 1 else [v for v in data]
        if data_type == 'long':
            data = struct.unpack('!{}l'.format(lenght // 4), data)
            return data[0] if lenght // 4 == 1 else [v for v in data]
        # 浮点小数4字节
        if data_type in ('real', 'float'):
            data = struct.unpack('!{}f'.format(lenght // 4), data)
            return data[0] if lenght // 4 == 1 else [v for v in data]
        if data_type in ('lreal', 'double'):
            data = struct.unpack('!{}d'.format(lenght // 8), data)
            return data[0] if lenght // 8 == 1 else [v for v in data]
        # 长长整形,8字节
        if data_type == 'ulonglong':
            data = struct.unpack('!{}Q'.format(lenght // 8), data)
            return data[0] if lenght // 8 == 1 else [v for v in data]
        if data_type == 'longlong':
            data = struct.unpack('!{}q'.format(lenght // 8), data)
            return data[0] if lenght // 8 == 1 else [v for v in data]

    @staticmethod
    def get_read_param(addrs):
        addr = addrs['addr'].upper().replace('I', 'I.').replace('Q', 'Q.').replace('M', 'M.').replace('V',
                                                                                                      'V.').replace(
                'DB', 'D.')
        lenght = int(addrs.get('len'))
        Type = addrs.get('type').lower()
        addr_list = addr.split('.')
        register = addr_list[0]
        for i, v in enumerate(addr_list):
            if i > 0:
                addr_list[i] = int(v)
        area = S7Client.get_area(register)
        if register in ('I', 'Q', 'M', 'V', 'T', 'C'):
            addr_list.insert(1, 1)
        if len(addr_list) > 3 and Type in ('bool', 'bit'):
            lenght = 1
        index = addr_list[-1] if Type in ('bool', 'bit') and len(addr_list) > 3 else None

        return [area, ] + addr_list[1:3] + [lenght, Type, index]

    # 获取area类型
    @staticmethod
    def get_area(string):
        areas = ADict({
                'I': 0x81,
                'Q': 0x82,
                'M': 0x83,
                'D': 0x84,
                'V': 0x84,
                'C': 0x1C,
                'T': 0x1D,
                })
        return areas.get(string)

    @staticmethod
    def parse_bit(bin_data, number = None):
        data = [[int(v) for v in bin(number)[2:].rjust(8, '0')[::-1]] for number in bin_data]
        if isinstance(number, int):
            return data[0][number]
        else:
            return data

    # 解析字符串
    @staticmethod
    def parse_string(bin_data):
        string_lenght = bin_data[1]
        if string_lenght == 0:
            return 0
        # 去除\x00
        data = bin_data.replace(b'\x00', b'')
        # 如果是可见字符就打印非可见就跳过1byte
        if data[0] > 31 and data[0] < 127:
            begin = 0
            end = string_lenght
        else:
            begin = 2
            end = string_lenght + 2
        string = data[begin:end].decode()
        return string

    # 读取配置文件
    def read_file(self, address_file):
        try:
            with open(address_file, 'r', newline = '') as file:
                reader = csv.DictReader(file)
                data = [dict(row) for row in reader]
                return data
        except Exception as e:
            print("读取地址文件失败:{},请检测文件:{}是否为csv格式及格式是否异常...\n".format(e, address_file))

    # 连接输入plc
    def connect_plc(self, msg):
        info = input(msg)
        if not info:
            self.connect(info)
        host, port, rack_slot = info.split(',')
        try:

            if ':' in rack_slot:
                rack, slot = rack_slot.split(':')
                self.connect(host, rack = int(rack), slot = int(slot), tcpport = int(port))
                return True
            else:
                self.set_connection_params(host, local_tsap = 0x1010, remote_tsap = int(rack_slot, 16))
                self.connect(host, rack = 0, slot = 0, tcpport = int(port))
                return True
        except Exception as e:
            print("连接PLC,地址:{},端口:{},板卡/插槽或TSAP:{},异常:{}\n".format(host, port, rack_slot, e))

    # 读取地址文件
    def read_address_file(self):
        path = input("请输入地址文件路径:\n")
        address_list = self.read_file(path)
        if not address_list:
            msg = "是否退出程序,q退出,其它继续..\n"
            self.read_input(msg)
            return

        msg = "输入PLC地址始,如 127.0.0.1,102,0:2或0x0102,前者是,板卡插槽后者TSAP:\n"
        if not self.connect_plc(msg):
            msg = "连接plc,失败请检测输入地址,q退出,其它继续..\n"
            self.read_input(msg)
            return

        error = None
        dir_paht, ext = os.path.splitext(path)
        file_path = "{}_检测结果{}".format(dir_paht, ext)
        with open(file_path, 'w+', newline = '') as file:
            headers = list(address_list[0].keys()) + ['检测结果']
            file = csv.DictWriter(file, headers)
            file.writeheader()
            for row in address_list:
                area = row.get('Area')
                area_number = row.get('AreaNum')
                addr = row.get('Addr')
                if area in ('I', 'Q', 'M', 'C', 'T'):
                    address = "{}{}".format(area, addr)
                elif area == 'V':
                    address = "DB1.{}".format(addr)
                else:
                    address = "{}{}.{}".format(area, area_number, addr)

                _type = row.get('DataType')
                lenght = row.get('DataLen') or '1'

                if not address or not _type or not lenght:
                    error = True
                    break
                config = {'addr': address, 'len': lenght, 'type': _type}
                result = self.read_value(config)
                print("地址:{},结果:{}\n".format(address, result))
                row.update({"检测结果": "{}".format(result)})
                file.writerow(row)
            os.system('start excel {}'.format(file_path))
            os.system('start wps {}'.format(file_path))
            if not error: return
            msg = "第一行,地址,长度,类型,分别是,Area,AreaNum,Addr,DataLen,DataType,请检测是否正确认或有空值,q键退出,其它继续\n"
            self.read_input(msg)

    # 读取数据
    def read_value(self, config):
        name = config.get('addr')[0].upper()
        param = self.get_read_param(config)
        area, db_number, begin, size, _type, _ = param
        _type = _type.lower()
        try:
            value = None
            if name == "I":
                value = self.ab_read(begin, size)

            if name == "Q":
                value = self.eb_read(begin, size)

            if name == "M":
                value = self.mb_read(begin, size)

            if name == "C":
                value = self.ct_read(begin, size)

            if name == "T":
                value = self.tm_read(begin, size)

            if name in ("V", "D"):
                db_number = 1 if name == 'V' else db_number
                value = self.db_read(db_number, begin, size)

            if value is not None:
                #self.log.debug('地址:{},值:{}'.format(config.get('addr'), value))
                value = self.parse_data(value, param)
                return value
        except Exception as e:
            msg = "{},请检测地址,类型或长度是否正确,读取plc地址：{}，出现异常".format(e,config.get('addr'))
            self.log.warning(msg)


    def read_input(self, msg):
        msg = input(msg)
        if msg.lower() == 'q':
            return
        else:
            self.read_address_file()


if __name__ == '__main__':
    S7 = S7Client()
    S7.read_address_file()
    # rec = S7.connect('192.100.100.154', tcpport = 102, rack = 0, slot = 1, )
    # addr = {'addr': 'DB100.1', 'len': 8, 'type': 'dword'}
    # param = S7.get_read_param(addr)
    # print(param)
    # data = S7.read_area(*param[:4])
    # value = S7.parse_data(data, param)
    # print(value)
    # while True:
    #     S7.get_connected()
    #     time.sleep(3)
    # S7.disconnect()